--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 161bf7e4136e682087013e18562f616df16108b8
Parents : 11be22f
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-22T23:29:35-05:00
feat(microphone): implement AudioWorklet for microphone processing and improve audio graph management
Changes
2 files changed, 59 insertions(+), 8 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.js b/meshchatx/src/frontend/js/MicrophoneRecorder.js
index 06661f16..b96272a8 100644
--- a/meshchatx/src/frontend/js/MicrophoneRecorder.js
+++ b/meshchatx/src/frontend/js/MicrophoneRecorder.js
@@ -6,12 +6,15 @@
* encode the audio to OGG/Opus using LXST's OpusFileSink without relying
* on ffmpeg or any browser MediaRecorder container output.
*/
+import microphoneRecorderWorkletUrl from "./MicrophoneRecorder.worklet.js?url";
+
class MicrophoneRecorder {
constructor() {
this.microphoneMediaStream = null;
this.audioContext = null;
this.sourceNode = null;
this.processorNode = null;
+ this.silentGainNode = null;
this.pcmChunks = [];
this.sampleRate = 0;
this.channels = 1;
@@ -32,11 +35,23 @@ class MicrophoneRecorder {
}
teardownAudioGraph() {
+ try {
+ if (this.processorNode?.port) {
+ this.processorNode.port.onmessage = null;
+ }
+ } catch {
+ // ignore
+ }
try {
this.processorNode?.disconnect?.();
} catch {
// ignore disconnect failures
}
+ try {
+ this.silentGainNode?.disconnect?.();
+ } catch {
+ // ignore disconnect failures
+ }
try {
this.sourceNode?.disconnect?.();
} catch {
@@ -53,6 +68,7 @@ class MicrophoneRecorder {
}
}
this.processorNode = null;
+ this.silentGainNode = null;
this.sourceNode = null;
this.audioContext = null;
}
@@ -88,24 +104,39 @@ class MicrophoneRecorder {
this.sourceNode = this.audioContext.createMediaStreamSource(this.microphoneMediaStream);
- if (typeof this.audioContext.createScriptProcessor !== "function") {
+ if (!this.audioContext.audioWorklet || typeof this.audioContext.audioWorklet.addModule !== "function") {
this.cleanupMediaStream();
this.teardownAudioGraph();
return false;
}
- const bufferSize = 4096;
- this.processorNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
- this.processorNode.onaudioprocess = (event) => {
- const input = event?.inputBuffer?.getChannelData(0);
- if (!input || input.length === 0) {
+ await this.audioContext.audioWorklet.addModule(microphoneRecorderWorkletUrl);
+ this.processorNode = new AudioWorkletNode(this.audioContext, "microphone-pcm-float", {
+ numberOfInputs: 1,
+ numberOfOutputs: 1,
+ channelCount: 1,
+ });
+ this.processorNode.port.onmessage = (event) => {
+ const buf = event.data;
+ if (!buf || typeof buf.byteLength !== "number" || buf.byteLength === 0) {
return;
}
- this.pcmChunks.push(new Float32Array(input));
+ this.pcmChunks.push(new Float32Array(buf.slice(0)));
};
this.sourceNode.connect(this.processorNode);
- this.processorNode.connect(this.audioContext.destination);
+ this.silentGainNode = this.audioContext.createGain();
+ this.silentGainNode.gain.value = 0;
+ this.processorNode.connect(this.silentGainNode);
+ this.silentGainNode.connect(this.audioContext.destination);
+
+ if (typeof this.audioContext.resume === "function") {
+ try {
+ await this.audioContext.resume();
+ } catch {
+ // ignore resume failures
+ }
+ }
return true;
} catch {
diff --git a/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js b/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js
new file mode 100644
index 00000000..aac7f2f3
--- /dev/null
+++ b/meshchatx/src/frontend/js/MicrophoneRecorder.worklet.js
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: 0BSD
+
+class MicrophonePcmFloatProcessor extends AudioWorkletProcessor {
+ process(inputs) {
+ const input = inputs[0];
+ if (!input || input.length === 0) {
+ return true;
+ }
+ const ch0 = input[0];
+ if (!ch0 || ch0.length === 0) {
+ return true;
+ }
+ const copy = new Float32Array(ch0.length);
+ copy.set(ch0);
+ this.port.postMessage(copy.buffer, [copy.buffer]);
+ return true;
+ }
+}
+
+registerProcessor("microphone-pcm-float", MicrophonePcmFloatProcessor);
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────